Data FetchingPatterns and Best Practices
hr.icon
これにより、以下のメリットがある
バックエンドのデータリソース(例:データベースなど)に直接アクセスできる。
感想:実際、BackendとFrontendが分かれている場合、このメリットは無いかな。
クライアントとサーバー間の往復通信やクライアントのメインスレッドの作業を減らすことで、データの取得とレンダリングを同じ環境で行うことができる。
感想:
メインスレッドのタスクが減るのは、すごくHappy
Server側の費用が増える可能性ありそうだけど、キャッシュがうまく利用できれば意外と気にならないかも?
クライアント上での複数個別リクエストの代わりに、一回の往復で複数のデータフェッチを行うことができる。
感想:
Server側で何度かリクエストした後、ページ生成したものを送るだけだから、Client側に1度送るだけというのは、そりゃそう。
### 必要な場所でのデータ取得
同じデータをツリー内の複数のコンポーネントで使用する必要がある場合、
例
同じデータ(例:現在のユーザー)をツリー内の複数のコンポーネントで使用する必要がある場合
親のレイアウトとその子コンポーネント間でデータを渡すことができないから。
Q. なぜ、同じデータを複数の場所で必要とする場合でも、各コンポーネントやレイアウトで必要に応じてデータを取得するのか?。何がHappyなのか?
なぜ実現できるのか?
感想
便利なのは分かる。
hr.icon
以下が実現
データを必要としないページの部分を即座にレンダリング
メリット
https://gyazo.com/ef21656accc3d39d08bc4a1c4120fee2
hr.icon
https://gyazo.com/c8295f2b825427b62cbddbca2d63d1b7
ルート内のリクエストが互いに依存しており、リクエストウォーターフォールを生み出す。
用途
一つのフェッチが他のフェッチの結果に依存している場合
次のフェッチを行う前に特定の条件を満たしたい場合
メリット
リソースを節約できる
主にメモリ
デメリット/注意:
この振る舞いは意図せず発生することもあり、ローディング時間を長くしてしまう原因となることもある
ルート内のリクエストが積極的に、そして同時に開始され、データを同時にロード
メリット
クライアントとサーバー間のウォーターフォールが減少し、データをロードするのに要する総時間が短縮
アプリケーションのパフォーマンスが向上し、ユーザーがより速く必要な情報にアクセスできるようになる
デメリット/注意
以下のパターンでに限り、適切に利用できる
すべてのリクエストが互いに独立しているとき
またはデータ取得において順序が重要でないとき
感想
hr.icon
例
Playlistsコンポーネントは、Artistコンポーネントがデータの取得を終え、artistIDプロップに依存してからでないとデータ取得を開始しない
async function Playlists({ artistID }: { artistID: string }) {
// プレイリストのデータを待つ
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// アーティストのデータを待つ
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
// q: artistの存在チェックしないの?何かいい感じに待ってくれるんだっけ?
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
データリクエストのブロッキング:
ウォーターフォールを防ぐ別のアプローチとして、アプリケーションのルートでデータをグローバルに取得する方法がある。
これはデータのロードが完了するまで、それ以下のすべてのルートセグメントのレンダリングをブロック。
「全てか無か」のデータ取得とも言える。
ページまたはアプリケーションの全データが揃っているか、一切ないか、のどちらかともいえる。
hr.icon
コンポーネント外でリクエストを定義して、それからコンポーネント内で呼びだす。
メリット
リクエストを並行して開始でき、時間を節約できる。
デメリット/懸念
両方のプロミスが解決されるまで、ユーザーはレンダリングされた結果を見ることができない
感想
これあまり良く分からない。
それぞれ並行にfetchして、それぞれ取得できれば、データ表示すればいいと思うんだけど。
await Promise.all([artistData, albumsData])でまとめる意味がわかんない。
例
getArtistとgetArtistAlbums関数をPageコンポーネントの外で定義し、コンポーネント内で呼び出して、両方のプロミスが解決されるのを待っている。
import Albums from './albums'
// NOTE: API Client側で定義するイメージね。
async function getArtist(username: string) {
const res = await fetch(https://api.example.com/artist/${username})
return res.json()
}
async function getArtistAlbums(username: string) {
const res = await fetch(https://api.example.com/artist/${username}/albums)
return res.json()
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 両方のリクエストを並列に開始
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// NOTE: 2つとも同じ箇所で管理するのか。
// プロミスが解決されるのを待つ
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
ユーザー体験を向上させるために、Suspence React境界を追加して、レンダリング作業を分割し、可能な限り早く結果の一部を表示できるようにすると良い (例で書いてくれたらよかったのに。)
code: suspense.tsx
<Suspense fallback={<div>artistName Loading...</div>}>
<h1>{artist.name}</h1>
</Suspense>
<Suspense fallback={<div>Albums Loading...</div>}>
<Albums list={albums}></Albums>
</Suspense>
hr.icon
方法
オプションとしてプリロード関数を作成
メリット
プリロード関数はパターンであってAPIではないので、任意の名前をつけることができる
q
何かnext.js側からimportするわけじゃないけど、どうやってpreloadの仕組みは動いているの?
codeだけ見ると、ただの関数に見えるけど。
code: components/Item.tsx
import { getItem } from '@/utils/get-item'
// preload関数を作成。
export const preload = (id: string) => {
// preload関数で、返り値が無いfetchを行う。(事前に叩いてキャッシュ機構をうまく利用する感じかな?)
// voidは与えられた式を評価し、undefinedを返します
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
// Component内でもgetItemでfetchしている。
const result = await getItem(id)
// ...
}
code:app/item/id/page.tsx import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// アイテムデータのロードを開始
preload(id)
// 別の非同期タスクを実行
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
a. サーバー上でのデータ取得関数がクライアント上で使用されないようにするために、server-onlyパッケージの使用を推奨する。
code: utils/get-item.ts
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
このアプローチを使うことで、データを積極的に取得し、レスポンスをキャッシュし、このデータ取得がサーバー上でのみ行われることを保証することができる。
utils/get-itemのエクスポートは、レイアウト、ページ、または他のコンポーネントによって使用され、アイテムのデータがいつ取得されるかを制御することができる。
hr.icon
#### Preventing sensitive data from being exposed to the client
目的
方法
taint react(taintObjectReferenceとtaintUniqueValue)を使用する アプリケーションでtaintingを有効にするには
Next.jsのconfigでexperimental.taintオプションをtrueに設定
その後、taintしたいオブジェクトや値をexperimental_taintObjectReferenceやexperimental_taintUniqueValue関数に渡す。
メリット
オブジェクトのインスタンス全体や機密値がクライアントに渡されるのを防ぐことができる。
感想
意図せず、client側に渡してしまう事増えそうだから、experimentalな機能でも利用したほうがいいんだろうな。
code:app/utils.ts
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'クライアントにユーザーオブジェクト全体を渡さないでください',
data
)
experimental_taintUniqueValue(
"クライアントにユーザーの住所を渡さないでください",
data,
data.address
)
return data
}
code: app/page.tsx
import { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // これはtaintObjectReferenceのためにエラーを引き起こします
address={userData.address} // これはtaintUniqueValueのためにエラーを引き起こします
/>
)
}
セキュリティとサーバーアクションについてもっと学ぶことをおすすめします